package lang4j.generator;

import lang4j.parser.IdentifierUtils;
import lang4j.parser.generated.*;
import org.apache.commons.lang.StringUtils;

import java.util.*;

/**
 * User: felix
 * Date: 28.10.2005
 * Time: 08:02:22
 */
public class Transformer {
    public static final String LIST_INTERFACE="java.util.List";

    final Lang4jGrammar grammar;
    int tokenNr = 0;

    final Map<Production, Collection<InterfaceProduction>> directSuperTypeMap;
    final Map<String, String> keywords;
    final Map<Object, String> tokens;

    final Map<TypeProduction, Collection<AcceptInfo>> acceptInfos;
    final Map<Production, Collection<JavaAttribute>> attributes;
    final private Map<Production, Collection<InterfaceProduction>> allSuperTypeMap;
    public static final String IDENTIFIER_REGEXP = "[_a-zA-Z][_a-zA-Z$0-9]*";

    public Transformer(final Lang4jGrammar grammar) {
        this.grammar = grammar;
        directSuperTypeMap = new HashMap<Production, Collection<InterfaceProduction>>();
        keywords = new HashMap<String, String>();
        acceptInfos=new HashMap<TypeProduction, Collection<AcceptInfo>>();
        tokens = new HashMap<Object, String>();
        attributes = new HashMap<Production, Collection<JavaAttribute>>();
        allSuperTypeMap = new HashMap<Production, Collection<InterfaceProduction>>();
        run();
    }


    public Collection<InterfaceProduction> getDirectSuperTypes(TypeProduction production) {
        if (!directSuperTypeMap.containsKey(production)) {
            directSuperTypeMap.put(production, new ArrayList<InterfaceProduction>());
        }
        return directSuperTypeMap.get(production);
    }

    public Collection<AcceptInfo> getAcceptInfos(TypeProduction production) {
            if (!acceptInfos.containsKey(production)) {
                acceptInfos.put(production, new LinkedList<AcceptInfo>());
            }
            return acceptInfos.get(production);
        }

    public Collection<InterfaceProduction> getAllSuperTypes(TypeProduction production) {
        if (!allSuperTypeMap.containsKey(production)) {
            //todo: make this a list to preserve ordering
            allSuperTypeMap.put(production, new ArrayList<InterfaceProduction>());
        }
        return allSuperTypeMap.get(production);
    }

    public Collection<JavaAttribute> getAttributes(TypeProduction production) {
        if (!attributes.containsKey(production)) {
            attributes.put(production, new LinkedList<JavaAttribute>());
        }
        return attributes.get(production);
    }

    public Map getKeywords() {
        return keywords;
    }

    public Map getTokens() {
        return tokens;
    }

    int getTokenNr() {
        tokenNr++;
        return tokenNr - 1;
    }

    void pushTerminal(Terminal t) {
        t.accept(new TerminalVisitor() {
            public void visitCharToken(CharToken charToken) {
                if (tokens.containsKey(charToken))
                    return;

                final String tokenIdent;
                if (IdentifierUtils.getIdentifier(charToken.getText())!=null){
                  tokenIdent=IdentifierUtils.getIdentifier(charToken.getText());
                } else
                  tokenIdent = "TOKEN_" + getTokenNr();
                tokens.put(charToken.getText(), tokenIdent);
            }

            public void visitKeyword(Keyword keyword) {
                if (keywords.containsKey(keyword.getText()))
                    return;
                if (!keyword.getText().matches(IDENTIFIER_REGEXP)){
                    throw new RuntimeException("Keyword '"+keyword.getText()+"' doesn't comply with identifier rules");
                }
                  keywords.put(keyword.getText(), "KEYWORD_"+IdentifierUtils.camelToUnderscore(keyword.getText()));

            }
        });
    }

    private void run() {
        pushTerminals();
        determineDirectSuperTypes();
        determineAllSuperTypes();
        determineConcreteTypeAttributes();
        determineInterfaceAttributes();
        determineAcceptInfosAnCheckListReferences();

    }

    /** Determines for each Concrete Type which Visitors have to be accepted and which Methods have to be accepted*/
    private void determineAcceptInfosAnCheckListReferences() {
        for (Production production: grammar.getProductions()) {
            production.accept(new ProductionVisitor() {
                public void visitEnumProduction(EnumProduction enumProduction) {
                }

                public void visitTypeProduction(TypeProduction typeProduction) {
                    typeProduction.accept(new TypeProductionVisitor() {
                        public void visitInterfaceProduction(InterfaceProduction interfaceProduction) {
                        }

                        public void visitConstructorProduction(ConstructorProduction constructorProduction) {
                            determineAcceptInfos(constructorProduction);
                        }


                    });
                }

                public void visitListProduction(ListProduction listProduction) {
                    listProduction.getType().accept(new ListElementTypeVisitor() {
                        public void visitProductionReference(ProductionReference productionReference) {
                        }

                        public void visitReferenceElement(ReferenceElement referenceElement) {
                            if (referenceElement.getProduction().getKeyField() == null)
                                throw new RuntimeException("Referenced Production '" + referenceElement.getProduction().getName() + "' has no key!");

                        }
                    });
                }
            });

        }

    }


    private void determineAcceptInfos(final ConstructorProduction constructorProduction) {

        Collection<InterfaceProduction> allSuperTypes = getAllSuperTypes(constructorProduction);
        for (InterfaceProduction interfaceProduction : allSuperTypes) {
            String visitorName = StringUtils.capitalize(interfaceProduction.getName());
            TypeProduction visitedProduction = determineVisited(interfaceProduction, constructorProduction, constructorProduction);
            getAcceptInfos(constructorProduction).add(new AcceptInfo(StringUtils.capitalize(visitedProduction.getName()), visitorName));
        }
    }

    private TypeProduction determineVisited(final InterfaceProduction interfaceProduction, final ConstructorProduction constructorProduction,TypeProduction current) {
    /*    System.out.println("Called determineVisited for "+interfaceProduction.getName()
                            +"Visitor accept in "+constructorProduction.getName()
                            +" current type:"+current.getName());*/
        if (interfaceProduction.getSubTypes().contains(current)){
            return current;
        }
        if (current==interfaceProduction)
            return null; //beware!
        for (TypeProduction typeProduction : getDirectSuperTypes(current)) {
            TypeProduction recurse = determineVisited(interfaceProduction, constructorProduction, typeProduction);
            if (recurse != null)
                return recurse;
        }
        return null;
    }

    /** determines all supertypes including those transitively referred by concrete classes and interfaces*/
    private void determineAllSuperTypes() {
        final Collection<Production> visited=new ArrayList<Production>(grammar.getProductions().size());
        for (Production production : grammar.getProductions()) {
            production.accept(new ProductionVisitor() {
                public void visitEnumProduction(EnumProduction enumProduction) {
                }

                public void visitTypeProduction(TypeProduction typeProduction) {
                    determineAllSuperTypes(typeProduction, visited);
                }

                public void visitListProduction(ListProduction listProduction) {
                }
            });
        }
    }

    private Collection<InterfaceProduction> determineAllSuperTypes(final TypeProduction typeProduction,Collection<Production> visited) {
        if (visited.contains(typeProduction))
            return getAllSuperTypes(typeProduction);
        visited.add(typeProduction);
        Collection<InterfaceProduction> superTypes=new ArrayList<InterfaceProduction>(getDirectSuperTypes(typeProduction));
        for (TypeProduction supertype : getDirectSuperTypes(typeProduction)) {
            superTypes.addAll(determineAllSuperTypes(supertype, visited));
        }

        getAllSuperTypes(typeProduction).addAll(superTypes);
        return superTypes;
    }


    private void determineInterfaceAttributes() {
        for (Production production : grammar.getProductions()) {
            production.accept(new ProductionVisitor() {
                public void visitEnumProduction(EnumProduction enumProduction) {
                }

                public void visitTypeProduction(TypeProduction typeProduction) {
                    typeProduction.accept(new TypeProductionVisitor() {
                        public void visitConstructorProduction(ConstructorProduction constructorProduction) {
                        }

                        public void visitInterfaceProduction(InterfaceProduction interfaceProduction) {
                            determineInterfaceAttributes(interfaceProduction);
                        }


                    });
                }

                public void visitListProduction(ListProduction listProduction) {
                }
            });

        }

    }

    private Collection<JavaAttribute> determineInterfaceAttributes(final InterfaceProduction production) {
        if (attributes.containsKey(production))
            return attributes.get(production);
        final List<JavaAttribute> interfaceAttributes = new ArrayList<JavaAttribute>(10);

        int i = 0;
        for (TypeProduction subtype : production.getSubTypes()) {

            final AttributeDeterminationVisitor visitor = new AttributeDeterminationVisitor();
            subtype.accept(visitor);
            if (i == 0)
                interfaceAttributes.addAll(visitor.getSubtypeAttribs());
            else
                interfaceAttributes.retainAll(visitor.getSubtypeAttribs());
            i++;
        }


        attributes.put(production, interfaceAttributes);
        return interfaceAttributes;

    }

    private void determineConcreteTypeAttributes() {
        for (Production production : grammar.getProductions()) {
            production.accept(new ProductionVisitor() {
                public void visitEnumProduction(EnumProduction enumProduction) {
                }

                public void visitTypeProduction(TypeProduction typeProduction) {
                    typeProduction.accept(new TypeProductionVisitor() {
                        public void visitInterfaceProduction(InterfaceProduction interfaceProduction) {
                        }

                        public void visitConstructorProduction(ConstructorProduction constructorProduction) {
                            getAttributes(constructorProduction).addAll(determineAttributes(constructorProduction.getFactors(), false));
                        }
                    });
                }

                public void visitListProduction(ListProduction listProduction) {
                }
            });

        }
    }

    private Collection<JavaAttribute> determineAttributes(final List<ProductionFactor> factors, final boolean optional) {
        final Collection<JavaAttribute> attributes = new LinkedList<JavaAttribute>();
        for (ProductionFactor productionFactor : factors) {

            productionFactor.accept(new ProductionFactorVisitor() {
                public void visitTerminal(Terminal terminal) {
                }

                public void visitAttribute(final Attribute attribute) {
                    attribute.accept(new AttributeVisitor() {
                        public void visitDeepAttribute(final DeepAttribute deepAttribute) {
                            final String name = deepAttribute.hasName() ? deepAttribute.getName() : deepAttribute.getProduction().getName();
                            deepAttribute.getProduction().accept(new ProductionVisitor() {
                                public void visitEnumProduction(EnumProduction enumProduction) {

                                    attributes.add(new JavaAttribute(name,
                                            StringUtils.capitalize(deepAttribute.getProduction().getName()),
                                            true, optional, attribute));
                                }

                                public void visitTypeProduction(TypeProduction typeProduction) {
                                    attributes.add(new JavaAttribute(name,
                                            StringUtils.capitalize(deepAttribute.getProduction().getName()),
                                            true, optional, attribute));
                                }

                                public void visitListProduction(ListProduction listProduction) {
                                    attributes.add(new JavaAttribute(name,
                                            "java.util.List<" + StringUtils.capitalize(listProduction.getType().getProduction().getName()) + ">",
                                            true, optional, attribute));
                                }
                            });

                        }

                        public void visitFlatAttribute(FlatAttribute flatAttribute) {
                            flatAttribute.accept(new FlatAttributeVisitor() {
                                public void visitIntLiteralAttribute(IntLiteralAttribute intLiteralAttribute) {
                                    attributes.add(new JavaAttribute(attribute.getName(), "int", false, optional, attribute));
                                }

                                public void visitDoubleLiteralAttribute(DoubleLiteralAttribute doubleLiteralAttribute) {
                                    attributes.add(new JavaAttribute(attribute.getName(), "double", false, optional, attribute));
                                }

                                public void visitCharLiteralAttribute(CharLiteralAttribute charLiteralAttribute) {
                                    attributes.add(new JavaAttribute(attribute.getName(), "char", false, optional, attribute));
                                }

                                public void visitBooleanLiteralAttribute(BooleanLiteralAttribute booleanLiteralAttribute) {
                                    attributes.add(new JavaAttribute(attribute.getName(), "boolean", false, optional, attribute));
                                }

                                public void visitStringLiteralAttribute(StringLiteralAttribute stringLiteralAttribute) {
                                    attributes.add(new JavaAttribute(attribute.getName(), "String", false, optional, attribute));
                                }

                                public void visitIdentifierAttribute(IdentifierAttribute identifierAttribute) {
                                    attributes.add(new JavaAttribute(attribute.getName(), "String", false, optional, attribute));
                                }

                                public void visitReferenceAttribute(ReferenceAttribute referenceAttribute) {
                                    if (referenceAttribute.getProduction().getKeyField() == null) {
                                        throw new RuntimeException("Referenced production '" + referenceAttribute.getProduction().getName() + "' has no key!");
                                    }
                                    attributes.add(new JavaAttribute(attribute.getName(),
                                            StringUtils.capitalize(referenceAttribute.getProduction().getName()),
                                            false, optional, attribute));
                                }

                                public void visitMultiLineStringAttribute(MultiLineStringAttribute multiLineStringAttribute) {
                                    attributes.add(new JavaAttribute(attribute.getName(), "String", false, optional, attribute));
                                }
                            });
                        }
                    });
                }

                public void visitOptionalAttributeList(OptionalAttributeList optionalAttributeList) {
                    attributes.addAll(determineAttributes(optionalAttributeList.getFactors(), true));
                }
            });
        }
        return attributes;
    }


    /** determines the supertypes and validates the production names*/
    private void determineDirectSuperTypes() {
        for (Production production : grammar.getProductions()) {
            if (Character.isUpperCase(production.getName().charAt(0))) {
                throw new RuntimeException("Invalid production name: '" + production.getName() + "'. Production names have to start with a lower case letter.");
            }

            production.accept(new ProductionVisitor() {
                public void visitEnumProduction(EnumProduction enumProduction) {
                    //rien du tout
                }

                public void visitTypeProduction(TypeProduction typeProduction) {
                    typeProduction.accept(new TypeProductionVisitor() {
                        public void visitInterfaceProduction(InterfaceProduction interfaceProduction) {
                            for (TypeProduction subType : interfaceProduction.getSubTypes()) {

                                getDirectSuperTypes(subType).add(interfaceProduction);
                            }
                        }

                        public void visitConstructorProduction(ConstructorProduction constructorProduction) {
                            //nix gibt nur interfaces als supertypen!
                        }
                    });
                }

                public void visitListProduction(ListProduction listProduction) {
                    //rien
                }
            });
        }
    }


    private void pushTerminals() {
        for (Production production : grammar.getProductions()) {
            production.accept(new ProductionVisitor() {
                public void visitEnumProduction(EnumProduction enumProduction) {
                    for (LabelledTerminal terminal : enumProduction.getValues()) {
                        pushTerminal(terminal.getTerminal());
                    }
                }

                public void visitListProduction(ListProduction listProduction) {
                    listProduction.accept(new ListProductionVisitor() {
                        public void visitListProductionPow(ListProductionPow listProductionPow) {
                            pushTerminal(listProductionPow.getLit());
                        }

                        public void visitListProductionStar(ListProductionStar listProductionStar) {
                            //rien
                        }
                    });
                }

                public void visitTypeProduction(TypeProduction typeProduction) {
                    typeProduction.accept(new TypeProductionVisitor() {
                        public void visitInterfaceProduction(InterfaceProduction interfaceProduction) {
                            //rien
                        }

                        public void visitConstructorProduction(ConstructorProduction constructorProduction) {
                            recursivePushTerminals(constructorProduction.getFactors());
                        }

                        private void recursivePushTerminals(final List<ProductionFactor> factors) {
                            for (ProductionFactor factor: factors) {
                                factor.accept(new ProductionFactorVisitor() {
                                    public void visitAttribute(Attribute attribute) {
                                    }

                                    public void visitOptionalAttributeList(OptionalAttributeList optionalAttributeList) {
                                        recursivePushTerminals(optionalAttributeList.getFactors());
                                    }

                                    public void visitTerminal(Terminal terminal) {
                                        pushTerminal(terminal);
                                    }
                                });
                            }
                        }
                    });
                }
            });
        }
    }

    private class AttributeDeterminationVisitor implements TypeProductionVisitor {
        Collection<JavaAttribute> subtypeAttribs;

        public Collection<JavaAttribute> getSubtypeAttribs() {
            return subtypeAttribs;
        }

        public void visitInterfaceProduction(InterfaceProduction interfaceProduction) {
            subtypeAttribs=determineInterfaceAttributes(interfaceProduction);

        }

        public void visitConstructorProduction(ConstructorProduction constructorProduction) {
            subtypeAttribs=getAttributes(constructorProduction);
        }
    }
}
